/*
Copyright (c) 2012 Safe Creative (http://www.safecreative.org)

Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
 */
package com.serialstamper.api.wrapper;

import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Map;

import javax.activation.MimetypesFileTypeMap;

import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.serialstamper.api.ApiException;
import com.serialstamper.api.SerialStamperAPI;
import com.serialstamper.api.util.AuthDigester;
import com.serialstamper.api.util.Digest;
import com.serialstamper.api.wrapper.converters.FileConverter;
import com.serialstamper.api.wrapper.converters.FileUserConverter;
import com.serialstamper.api.wrapper.converters.ListPageConverter;
import com.serialstamper.api.wrapper.converters.ModelConverter;
import com.serialstamper.api.wrapper.converters.OrganizationDataConverter;
import com.serialstamper.api.wrapper.converters.StringMapConverter;
import com.serialstamper.api.wrapper.converters.UploadLookupRequestConverter;
import com.serialstamper.api.wrapper.converters.UploadLookupResponseConverter;
import com.serialstamper.api.wrapper.converters.UserAuthConverter;
import com.serialstamper.api.wrapper.converters.UserConverter;
import com.serialstamper.api.wrapper.model.File;
import com.serialstamper.api.wrapper.model.FileUser;
import com.serialstamper.api.wrapper.model.HashAlgorithm;
import com.serialstamper.api.wrapper.model.OrganizationData;
import com.serialstamper.api.wrapper.model.UploadLookupRequest;
import com.serialstamper.api.wrapper.model.UploadLookupResponse;
import com.serialstamper.api.wrapper.model.User;
import com.serialstamper.api.wrapper.model.UserAuth;
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.converters.Converter;
import com.thoughtworks.xstream.io.naming.NoNameCoder;
import com.thoughtworks.xstream.io.xml.XppDriver;

/**
 * SerialStamperAPI main wrapper
 *
 * @author mpolo@safecreative.org
 * @author jguillo@safecreative.org
 */
public class SerialStamperAPIWrapper {

    public final static String DEFAULT_API_URL = "https://api.serialstamper.com";
    public final static String ARENA_URL = "http://test.serialstamper.com:8080";

	public enum SignatureType { NONE, SHARED_KEY, AUTH_KEY }
	
    private static Logger log = LoggerFactory.getLogger(SerialStamperAPIWrapper.class);
    private final static String STATE_READY = "ready";
    private SerialStamperAPI api;
    private String baseUrl;
    private UserAuth auth;
    private Map<Class, ModelConverter> convertersMap = new HashMap<Class, ModelConverter>();

    /**
     * Constructor
     *
     * @param sharedKey api's shared key
     * @param privateKey api's privatekey key
     */
    public SerialStamperAPIWrapper(String sharedKey, String privateKey) {
        this(new SerialStamperAPI(sharedKey, privateKey));
    }

    
    /**
     * Constructor with shared public key
     *
     * @param sharedKey api's shared key     
     */
    public SerialStamperAPIWrapper(String sharedKey) {
        this(new SerialStamperAPI(sharedKey, null));
    }   
    
    /**
     * Constructor with authorization keys
     *
     * @param sharedKey api's shared key
     * @param auth authorization keys
     */
    public SerialStamperAPIWrapper(String sharedKey, UserAuth auth) {
        this(new SerialStamperAPI(sharedKey, null));
        this.auth = auth;
    }

    /**
     * Constructor with authorization keys
     *
     * @param sharedKey api's shared key
     * @param auth authorization keys
     */
    public SerialStamperAPIWrapper(String sharedKey, String authKey, String authPrivateKey) {
        this(sharedKey, new UserAuth(authKey, authPrivateKey));
    }

    /**
     * Constructor using low level api interface
     * @param api low level interface
     */
    public SerialStamperAPIWrapper(SerialStamperAPI api) {
        this.api = api;        
        convertersMap.put(User.class, new UserConverter());		
		convertersMap.put(UserAuth.class, new UserAuthConverter());	
        convertersMap.put(File.class, new FileConverter());
        convertersMap.put(OrganizationData.class, new OrganizationDataConverter());
        convertersMap.put(UploadLookupRequest.class, new UploadLookupRequestConverter());
        convertersMap.put(UploadLookupResponse.class, new UploadLookupResponseConverter());
        convertersMap.put(FileUser.class, new FileUserConverter());
    }

    /**
     * Get low level api interface
     *
     * @return low level api interface
     */
    public SerialStamperAPI getApi() {
        return api;
    }

    /**
     * Sets api endpoint url
     *
     * @param baseUrl api endpoint url
     */
    public void setBaseUrl(String baseUrl) {
        this.baseUrl = baseUrl;
        setApiUrl();
    }

    /**
     * Gets api endpoint url
     *
     * @return api endpoint url
     */
    public String getBaseUrl() {
        return StringUtils.defaultIfEmpty(baseUrl, DEFAULT_API_URL);
    }

    /**
     * Gets user authorization keys
     * 
     * @return UserAuth object
     */
	public UserAuth getAuth() {
		return auth;
	}

    /**
     * Sets user authorization keys
     * 
     * @param auth UserAuth object
     */
	public void setAuth(UserAuth auth) {
		this.auth = auth;
	}
    
    ////////////////////////////////////////////////////////////////////////////
    // Information methods
    ////////////////////////////////////////////////////////////////////////////

    /**
     * Retrieves current api version. This method can also be used to check api
     * access.
     * @return api's current version
     * @throws ApiException
     */
    public String getVersion() throws ApiException {
        if (api.getBaseUrl() == null) {
            api.setBaseUrl(getBaseUrl());
        }
        String result = api.callGet("version", false);
        String version = api.evalXml(result, "/version");
        return version;
    }

    ////////////////////////////////////////////////////////////////////////////
    // Organization data
    ////////////////////////////////////////////////////////////////////////////

    /**
     * Retrieves information about the organization.
     * This method can be used to check signature access
     * @return
     * @throws ApiException
     */
    public OrganizationData getOrganizationData() throws ApiException {
    	 if (api.getBaseUrl() == null) {
             api.setBaseUrl(getBaseUrl());
         }
    	 return getObject("organizationData", OrganizationData.class);
    }
    
    /**
     * Modifies the organization account data.
     * @param OrganizationData object
     * @return
     * @throws ApiException
     */
    public void setOrganizationData(OrganizationData data) throws ApiException {
    	 if (api.getBaseUrl() == null) {
             api.setBaseUrl(getBaseUrl());
         }
    	 putObject("organizationData", data);
    }
    
    /**
     * Change the client callback URL
     * @param String callbackUrl
     * @throws ApiException
     */
    public void setCallbackUrl(String callbackUrl) throws ApiException {
    	 OrganizationData data = new OrganizationData();
    	 data.setCallbackUrl(callbackUrl);
    	 setOrganizationData(data);
    }
    
    ///////////////////////////////////////////////////////////////////////
    // java.io.File registration high-level methods 
    ///////////////////////////////////////////////////////////////////////
    public File registerFile(java.io.File f) throws ApiException {
    	return registerFile(f, null);
    }

    public File registerFile(java.io.File f, UploadProgressListener listener) throws ApiException {
    	return registerFile(f, f.getName(), null, listener);
    }

    public File registerFile(java.io.File f, String title, Map<String, String> userMeta) throws ApiException {
    	return registerFile(f, title, userMeta, null);
    }
    
    public File registerFile(java.io.File f, String title, Map<String, String> userMeta, UploadProgressListener listener) throws ApiException {
    	return registerFile(f, guessMimeType(f), title, userMeta, listener);
    }

	public File registerFile(java.io.File f, String contentType, String title, Map<String, String> userMeta) throws ApiException {
		return registerFile(f, contentType, title, userMeta, null);
	}
	public File registerFile(java.io.File f, String contentType, String title, Map<String, String> userMeta, UploadProgressListener listener) throws ApiException {
    	try {
    		UploadLookupResponse uploadInfo = uploadLookup(f.getName(), contentType);
    		api._uploadFile(f, uploadInfo.getUploadUrl(), contentType, listener);
    		File registeredFile = new File();
    		registeredFile.setName(title);
    		registeredFile.setStorageKey(uploadInfo.getStorageKey());
    		if (userMeta != null) {
    			registeredFile.getMetadata().putAll(userMeta);
    		}
    		return newFile(registeredFile);
    	}
    	catch (ApiException e) {
    		throw e;
    	}
    	catch (Exception e) {
    		throw new ApiException(e);
    	}
    }
    

    
    ///////////////////////////////////////////////////////////////////////
    // Text registration high-level methods 
    ///////////////////////////////////////////////////////////////////////
    public File registerText(String text, String title) throws ApiException {
    	return registerText(text, title, null);
    }
    public File registerText(String text, String title, Map<String, String> userMeta) throws ApiException {
    	return registerText(text, title, "utf-8", userMeta);
    }
    public File registerText(String text, String title, String charset, Map<String, String> userMeta) throws ApiException {
    	return registerText(text, title, "text/plain", charset, userMeta);
    }
    public File registerText(String text, String title, String contentType, String charset, Map<String, String> userMeta) throws ApiException {
    	try {
    		contentType = contentType.toLowerCase();
    		charset = charset.toLowerCase();
    		String fullContentType = contentType;
    		if (!StringUtils.isEmpty(charset)) {
    			fullContentType += "; charset=" + charset;
    		}
    		UploadLookupResponse uploadInfo = uploadLookup(title + ".txt", fullContentType);
    		api._uploadText(text, uploadInfo.getUploadUrl(), contentType, charset);
    		File registeredFile = new File();
    		registeredFile.setName(title);
    		registeredFile.setStorageKey(uploadInfo.getStorageKey());
    		if (userMeta != null) {
    			registeredFile.getMetadata().putAll(userMeta);
    		}
    		return newFile(registeredFile);
    	}
    	catch (ApiException e) {
    		throw e;
    	}
    	catch (Exception e) {
    		throw new ApiException(e);
    	}
    }


    
    
    ///////////////////////////////////////////////////////////////////////
    // URL registration high-level methods 
    ///////////////////////////////////////////////////////////////////////
    public File registerURL(String url) throws ApiException, MalformedURLException {
    	return registerURL(url, null);
    }
    
    public File registerURL(String url, Map<String, String> userMeta) throws ApiException, MalformedURLException {
    	return registerURL(url, getFilenameFromUrl(url), userMeta);
    }
    
    public File registerURL(String url, String title, Map<String, String> userMeta) throws ApiException {
    	try {
    		File f = new File();
    		f.setSourceUrl(url);
    		f.setName(title);
    		if (userMeta != null) {
    			f.getMetadata().putAll(userMeta);
    		}
    		return newFile(f);
    	}
    	catch (ApiException e) {
    		throw e;
    	}
    	catch (Exception e) {
    		throw new ApiException(e);
    	}
    }

    ///////////////////////////////////////////////////////////////////////
    // Upload lookup
    ///////////////////////////////////////////////////////////////////////
    public UploadLookupResponse uploadLookup(String filename) throws ApiException {
    	return uploadLookup(filename, "application/octet-stream");
    }
    
    public UploadLookupResponse uploadLookup(String filename, String contentType) throws ApiException {
    	UploadLookupRequest request = new UploadLookupRequest();
    	request.setFilename(filename);
    	request.setContentType(contentType);
    	String res = postObject("uploadLookup", request, SignatureType.AUTH_KEY);
    	return readObject(UploadLookupResponse.class, res); 
    }
    
    
    ///////////////////////////////////////////////////////////////////////
    // Upload form
    ///////////////////////////////////////////////////////////////////////
    public String getUploadFormURL(String returnUrl) throws ApiException {
    	return getUploadFormURL(returnUrl, -1);
    }
    
    public String getUploadFormURL(String returnUrl, int timeoutSeconds) throws ApiException {
    	return getUploadFormURL(returnUrl, timeoutSeconds, "html", null);
    }
    
    public String getUploadFormJsURL(String returnUrl) throws ApiException {
    	return getUploadFormJsURL(returnUrl, null);
    }
    
    public String getUploadFormJsURL(String returnUrl, int timeoutSeconds) throws ApiException {
    	return getUploadFormJsURL(returnUrl, timeoutSeconds, null);
    }
    
    public String getUploadFormJsURL(String returnUrl, String jsonpCallback) throws ApiException {
    	return getUploadFormJsURL(returnUrl, -1, jsonpCallback);
    }
    
    public String getUploadFormJsURL(String returnUrl, int timeoutSeconds, String jsonpCallback) throws ApiException {
    	return getUploadFormURL(returnUrl, timeoutSeconds, "json", jsonpCallback);
    }
    
    public String getUploadFormURL(String returnUrl, int timeoutSeconds, String format, String jsonp) throws ApiException {
    	try {
	    	String ztime = api.getZTime();
	    	String privatekey;
	    	if (auth == null) {
	    		privatekey = api.getPrivateKey();
			} else {
	    		privatekey = auth.getAuthPrivateKey();
			}
			AuthDigester digest = new AuthDigester(privatekey);
			if (auth != null) {
				digest.update(auth.getAuthKey());
				digest.update("\n");				
			}
			digest.update(api.getSharedKey());
			digest.update("\n");
			digest.update(ztime);
			digest.update("\n");
			if (returnUrl != null) {
				digest.update(returnUrl);
			}
			digest.update("\n");
			if (timeoutSeconds > 0) {
				digest.update(String.valueOf(timeoutSeconds));
			}
			digest.update("\n");
			String signature = digest.digest();
			
			StringBuilder url = new StringBuilder(api.getBaseUrl());
			url.append("/upload-form");
			url.append("?sc-sharedkey=").append(api.getSharedKey());
			if (auth != null) {
				url.append("&sc-authkey=").append(auth.getAuthKey());
			}
			url.append("&sc-ztime=").append(ztime);
			if (returnUrl != null)
				url.append("&returnUrl=").append(URLEncoder.encode(returnUrl, "UTF-8"));
			if (timeoutSeconds > 0)
				url.append("&timeout=").append(timeoutSeconds);
			url.append("&sc-signature=").append(signature);
			
			if (format != null) {
				url.append("&format=").append(format);
			}
			if (jsonp != null) {
				url.append("&jsonp=").append(jsonp);
			}
			
			String sUrl = url.toString();
			
			log.debug("Upload form URL: {}", sUrl);
			
	    	return sUrl;
    	}
    	catch (Exception ex) {
    		throw new ApiException(ex);
    	}
    }
    
    ///////////////////////////////////////////////////////////////////////
    // File methods
    ///////////////////////////////////////////////////////////////////////
    
    public File getFile(String id) throws ApiException {
    	try {
    		return getObject("file/" +id, File.class, SignatureType.AUTH_KEY);
    	}
    	catch (ApiException e) {
    		if ("FileNotFound".equals(e.getErrorCode())) {
    			return null;
    		}
    		else {
    			throw e;
    		}
    	}
    }
    
    public File newFile(File file) throws ApiException {
    	String res = postObject("file", file, SignatureType.AUTH_KEY);
    	String id = api.evalXml(res, "/fileId");
    	file.setId(id);
    	return file;
    }
    
    public void updateFile(File file) throws ApiException {
    	putObject("file/" +file.getId(), file, SignatureType.AUTH_KEY);
    }

    public void deleteFile(String id) throws ApiException {
    	delete("file/" +id, SignatureType.AUTH_KEY);
    }
    
    public ListPage<File> listFiles(int page) throws ApiException {
    	return listFiles(page,null);
    }
	
    public ListPage<File> listFiles(int page,Map<String, String> filter) throws ApiException {
		return listFiles(page, filter, false);
    }
    
    public ListPage<File> listFiles(int page,Map<String, String> filter, boolean includeUsers) throws ApiException {
		return getListPage(File.class,
				includeUsers ? "filesWithUsers" : "files",
				SignatureType.AUTH_KEY,page,filter);
    }
    
    public ListPage<File> listUserFiles(String userId, int page) throws ApiException {
    	return listUserFiles(userId, page,null);
    }
	
    public ListPage<File> listUserFiles(String userId, int page,Map<String, String> filter) throws ApiException {
		return getListPage(File.class,"filesByUser/" + userId,SignatureType.SHARED_KEY,page,filter);
    }
    
    ///////////////////////////////////////////////////////////////////////
    // FileUser methods
    ///////////////////////////////////////////////////////////////////////
    
    public FileUser getFileUser(String fileId, String userId) throws ApiException {
    	try {
    		FileUser fu = getObject("fileUser/" + fileId + "/" + userId, FileUser.class, SignatureType.AUTH_KEY);
    		fu.setFileId(fileId);
    		return fu;
    	}
    	catch (ApiException e) {
    		if ("FileNotFound".equals(e.getErrorCode()) || "UserNotFound".equals(e.getErrorCode())) {
    			return null;
    		}
    		else {
    			throw e;
    		}
    	}
    }
    
    public void newFileUser(FileUser fileUser) throws ApiException {
    	checkPrivateKey();
    	String res = postObject("fileUser/" + fileUser.getFileId(), fileUser, SignatureType.SHARED_KEY);
    	checkReady(res);
    }
    
    public void updateFileUser(FileUser fileUser) throws ApiException {
    	putObject("fileUser/" + fileUser.getFileId() + "/" + fileUser.getUserId(), fileUser, SignatureType.AUTH_KEY);
    }

    public void deleteFileUser(String fileId, String userId) throws ApiException {
    	checkPrivateKey();
    	delete("fileUser/" + fileId + "/" + userId, SignatureType.SHARED_KEY);
    }
    
    public ListPage<FileUser> listFileUsers(String fileId, int page) throws ApiException {
    	ListPage<FileUser> list = getListPage("fileUsers/" + fileId + "/" + String.valueOf(page), FileUser.class, SignatureType.AUTH_KEY);
    	for (FileUser fu : list.getList()) {
    		fu.setFileId(fileId);
    	}
    	return list;
    }
    
    ///////////////////////////////////////////////////////////////////////
    // Download
    ///////////////////////////////////////////////////////////////////////
    public String getFileDownloadUrl(String fileId) throws ApiException {
    	return getFileDownloadUrl(fileId, 0);
    }

    public String getFileDownloadUrl(String fileId, int timeoutSeconds) throws ApiException {
    	String path = "fileDownload/" + fileId;
    	if (timeoutSeconds > 0) {
    		path += "?timeout=" + String.valueOf(timeoutSeconds);
    	}
    	String result;
    	if (auth == null) {
    		result = api.callGet(path);
    	}
    	else {
    		result = api.callGet(path, auth.getAuthKey(), auth.getAuthPrivateKey());
    	}
    	checkError(result);
    	return api.evalXml(result, "downloadUrl"); 
    }
    
    
    ///////////////////////////////////////////////////////////////////////
    // Timestamp
    ///////////////////////////////////////////////////////////////////////
    public byte[] timestampText(String text, HashAlgorithm algorithm) throws ApiException {
    	try {
    		byte[] contents = text.getBytes("UTF-8");
    		return timestampBytes(contents, algorithm);
    	}
    	catch (UnsupportedEncodingException e) {
    		throw new RuntimeException(e);
    	}
    }
    
    public byte[] timestampBytes(byte[] content, HashAlgorithm algorithm) throws ApiException {
    	byte[] hash = Digest.getBytesDigest(content, algorithm.name());
    	return timestampHash(hash, algorithm);
    }
    
    public byte[] timestampHash(byte[] hash, HashAlgorithm algorithm) throws ApiException {
    	return timestampHash(Digest.toHex(hash), algorithm);
    }
    
    public byte[] timestampHash(String hash, HashAlgorithm algorithm) throws ApiException {
    	String res = api.callPost("timestamp", 
    			"<timestampRequest>" +
    					"<algorithm>" + algorithm.name() + "</algorithm>" +
    					"<hash>" + hash + "</hash>" +
    			"</timestampRequest>");
    	checkError(res);
    	String timestamp = api.evalXml(res, "timestamp");
    	return Digest.fromHex(timestamp);
    }
	
	////////////////////////////////////////////////////////////////////////////
    // User
    ////////////////////////////////////////////////////////////////////////////
	
	public UserAuth authUser(String name,String password) throws ApiException {
		return authUser(new User(name,password));
	}
	
	public UserAuth authUser(User user) throws ApiException {
    	try {
    		String response = postObject("userAuth", user, SignatureType.NONE);
			checkError(response);
			return readObject(UserAuth.class, response);
    	} catch (ApiException e) {
    		if ("UserNotFound".equals(e.getErrorCode())) {
    			return null;
    		} else {
    			throw e;
    		}
    	}		
	}
	
	public User getAuthorizedUser() throws ApiException {
		return getUser("");
		
	}
	public UserAuth getUserAuth(String userId) throws ApiException {
		try {
    		return getObject("adminUserAuth/" + userId, UserAuth.class, SignatureType.SHARED_KEY);
    	} catch (ApiException e) {
    		if ("UserNotFound".equals(e.getErrorCode())) {
    			return null;
    		} else {
    			throw e;
    		}
    	}
	}
	
	public User getUser(User user) throws ApiException {
		return user == null || user.getId() == null ? null : getUser(user.getId());
	}
	
	public User getUser(String id) throws ApiException {
    	try {
    		String path;
    		if (StringUtils.isEmpty(id)) {
    			path = "user";
    		}
    		else {
    			path = "user/" + id;
    		}
    		return getObject(path, User.class, SignatureType.AUTH_KEY);
    	} catch (ApiException e) {
    		if ("UserNotFound".equals(e.getErrorCode())) {
    			return null;
    		} else {
    			throw e;
    		}
    	}
    }
    
	public User newUser(String name, String password, String fullName, String mail) throws ApiException {
		return newUser(new User(name, password, fullName, mail));
	}
	
    public User newUser(User user) throws ApiException {
    	String res = postObject("user", user, SignatureType.AUTH_KEY);
		String id = api.evalXml(res, "/userId");
		user.setId(id);
		return user;
    }
    
    public void updateUser(User user) throws ApiException {
    	putObject("user/" +user.getId(), user, SignatureType.AUTH_KEY);
    }
	
	public boolean deleteUser(User user) throws ApiException {
		return deleteUser(user.getId());
	}

    public boolean deleteUser(String id) throws ApiException {
		checkPrivateKey();
    	return delete("user/" +id, SignatureType.SHARED_KEY);
    }
	
    public ListPage<User> listUsers(int page) throws ApiException {
    	return listUsers(page,null);
    }
	
    public ListPage<User> listUsers(int page,Map<String, String> filter) throws ApiException {
		checkPrivateKey();
		return getListPage(User.class,"users",SignatureType.SHARED_KEY,page,filter);
    }  
    
    //////////////////////////////////////////////////////////
    // Standard object operations
    //////////////////////////////////////////////////////////
    
    public <T> T getObject(String path, Class<T> clazz) throws ApiException {
    	return getObject(path, clazz, SignatureType.SHARED_KEY);
    }

    public <T> T getObject(String path, Class<T> clazz, SignatureType signType) throws ApiException {
    	return getObject(path, clazz, convertersMap.get(clazz), signType);
    }
    
    public <T> T getObject(String path, Class<T> clazz, ModelConverter converter, SignatureType signType) throws ApiException {
    	String response;
    	if (signType == SignatureType.NONE) {
    		response = api.callGet(path, false);
    	}
    	else if (signType == SignatureType.AUTH_KEY && auth != null) {
    		response = api.callGet(path, auth.getAuthKey(), auth.getAuthPrivateKey());
    	}
    	else {
    		response = api.callGet(path);
    	}
    	
    	checkError(response);
    	return readObject(clazz, response, converter);
    }
	
	private  <T> ListPage<T> getListPage(Class<T> modelClass, String listEndpoint, SignatureType signatureType,int page,Map<String, String> filter) throws ApiException {    
    	try {
	    	StringBuilder path = new StringBuilder(listEndpoint + "/");
	    	path.append(page);
	    	if (filter != null) {
	    		path.append("?");
	    		for (Map.Entry<String, String> entry : filter.entrySet()) {
	    			path.append(URLEncoder.encode(entry.getKey(), "UTF-8"));
	    			path.append("=");
	    			path.append(URLEncoder.encode(entry.getValue(), "UTF-8"));
	    			path.append("&");
	    		}
		    	path.setLength(path.length() - 1);
	    	}
	    	return getListPage(path.toString(), modelClass, signatureType);
    	}
    	catch (UnsupportedEncodingException e) {
    		throw new RuntimeException(e);
    	}
    }	
    
    public <T> ListPage<T> getListPage(String path, Class<T> clazz) throws ApiException {
    	return getListPage(path, clazz, SignatureType.SHARED_KEY);
    }
    public <T> ListPage<T> getListPage(String path, Class<T> clazz, SignatureType signType) throws ApiException {
    	return getListPage(path, clazz, convertersMap.get(clazz), signType);    	
    }
    public <T> ListPage<T> getListPage(String path, Class<T> clazz, ModelConverter converter, SignatureType signType) throws ApiException {
    	String response;
    	if (signType == SignatureType.NONE) {
    		response = api.callGet(path, false);
    	}
    	else if (signType == SignatureType.AUTH_KEY && auth != null) {
    		response = api.callGet(path, auth.getAuthKey(), auth.getAuthPrivateKey());
    	}
    	else {
    		response = api.callGet(path);
    	}
    	
    	checkError(response);
    	return readListPage(response, clazz, converter);
    }
    
    public String postObject(String path, Object obj) throws ApiException {
    	return postObject(path, obj, SignatureType.SHARED_KEY);
    }
    public String postObject(String path, Object obj, SignatureType signType) throws ApiException {
    	return postObject(path, obj, convertersMap.get(obj.getClass()), signType);
    }
    public String postObject(String path, Object obj, ModelConverter converter, SignatureType signType) throws ApiException {
    	return postObject(path, obj, converter, converter.getAlias(), signType);
    }
    public String postObject(String path, Object obj, Converter converter, String alias, SignatureType signType) throws ApiException {
    	XStream xs = createPutSchema(obj.getClass(), converter, alias);
    	
    	String res;
    	if (signType == SignatureType.NONE) {
    		res = api.callPost(path, xs.toXML(obj), false);
    	}
    	else if (signType == SignatureType.AUTH_KEY && auth != null) {
    		res = api.callPost(path, xs.toXML(obj), auth.getAuthKey(), auth.getAuthPrivateKey());
    	}
    	else {
    		res = api.callPost(path, xs.toXML(obj));
    	}

    	checkError(res);
        return res;
    }
    
    public String putObject(String path, Object obj) throws ApiException {
    	return putObject(path, obj, SignatureType.SHARED_KEY);
    }
    public String putObject(String path, Object obj, SignatureType signType) throws ApiException {
    	return putObject(path, obj, convertersMap.get(obj.getClass()), signType);
    }
    public String putObject(String path, Object obj, ModelConverter converter, SignatureType signType) throws ApiException {
    	return putObject(path, obj, converter, converter.getAlias(), signType);
    }
    public String putObject(String path, Object obj, Converter converter, String alias, SignatureType signType) throws ApiException {
    	if (signType == SignatureType.NONE)
    		throw new IllegalArgumentException("SignatureType.NONE");
    	
    	XStream xs = createPutSchema(obj.getClass(), converter, alias);
        
    	String res;
    	if (signType == SignatureType.AUTH_KEY && auth != null) {
    		res = api.callPut(path, xs.toXML(obj), auth.getAuthKey(), auth.getAuthPrivateKey());
    	}
    	else {
    		res = api.callPut(path, xs.toXML(obj));
    	}
    	checkError(res);
        return res;
    }
    
    public boolean delete(String path) throws ApiException {
        return delete(path, SignatureType.SHARED_KEY);
    }
    
    public boolean delete(String path, SignatureType signType) throws ApiException {
    	if (signType == SignatureType.NONE)
    		throw new IllegalArgumentException("SignatureType.NONE");
    	String res;
    	if (signType == SignatureType.AUTH_KEY && auth != null) {
    		res = api.callDelete(path, auth.getAuthKey(), auth.getAuthPrivateKey());
    	}
    	else {
    		res = api.callDelete(path);
    	}
		checkError(res);
        return checkReady(res);
    }
    
    
    ///////////////////////////////////////////////////////////////////////
    // Low level methods
    ///////////////////////////////////////////////////////////////////////
	public void checkPrivateKey() throws ApiException {
		if(StringUtils.isEmpty(getApi().getPrivateKey())) {
			//In case we are called from a shared + user auth wrapper
			throw new ApiException(SerialStamperAPI.NOT_AUTHORIZED_ERROR,"No privateKey");
		}		
	}
    public boolean checkReady(String response) throws ApiException {
        return checkState(response, STATE_READY);
    }

    public boolean checkState(String response, String expected) throws ApiException {
        try {
            String state = api.getResponseState(response);
            return expected.equals(state);
        } catch (Exception ex) {
            throw new ApiException(ex);
        }
    }

    public void checkError(String response) throws ApiException {
        if (api.isError(response)) {
            try {
                String errorCode = api.getErrorCode(response);
                String errorMessage = api.getErrorMessage(response);
                throw new ApiException(errorCode, errorMessage);
            } catch (Exception ex) {
                if (ex instanceof ApiException) {
                    throw (ApiException) ex;
                }
                throw new ApiException(ex);
            }
        }
    }

    @SuppressWarnings("unchecked")
    public <T extends Object> ListPage<T> readListPage(String response,Class<T> clazz,Converter converter) {
        XStream xs = createSchema();        
        xs.registerConverter(new ListPageConverter<T>(clazz,converter));                
        ListPage<T> listPage = readObject(ListPage.class, "listpage", response, xs);        
        return listPage;
    }

    @SuppressWarnings("unchecked")
    private <T> T readObject(Class<T> clazz, String response) {
        return readObject(clazz, response, convertersMap.get(clazz));
    }

    @SuppressWarnings("unchecked")
    private <T> T readObject(Class<T> clazz, String response, ModelConverter converter) {
        XStream xs = createSchema();
        if(converter == null) {
        	converter = new ModelConverter(clazz);
        }
        xs.registerConverter(converter);
        return readObject(clazz, converter.getAlias(), response, xs);
    }

    @SuppressWarnings("unchecked")
    <T> T readObject(Class<T> clazz, String alias, String response,XStream xs) {
        if(xs == null)  {
            xs = createSchema();
        }
        xs.alias(alias, clazz);
        T result = null;
        try {
            result = (T) xs.fromXML(response);
        } catch (Exception e) {
            log.error("Parsing xml response", e);
        }
        return result;
    }

    protected void setApiUrl() {
        api.setBaseUrl(getBaseUrl());
    }

    private XStream createSchema() {
    	XStream xs = new XStream(new XppDriver(new NoNameCoder()));
    	xs.registerConverter(new StringMapConverter());
    	return xs;
    }
    private XStream createPutSchema(Class clazz, Converter converter, String alias) {
    	XStream xs = createSchema();
    	xs.registerConverter(new StringMapConverter());
        if(converter != null) {
            xs.registerConverter(converter);
        }
        if(alias != null) {
            xs.alias(alias, clazz);
        }
        return xs;
    }
    
    private MimetypesFileTypeMap mimetypesMap = new MimetypesFileTypeMap();
    private String guessMimeType(java.io.File f) {
		return mimetypesMap.getContentType(f);
	}

    private String getFilenameFromUrl(String urlString) throws MalformedURLException {
    	URL url = new URL(urlString);
    	String path = url.getPath();
    	int index = path.lastIndexOf('/');
    	if (index < 0) {
    		return path;
    	}
    	else {
    		return path.substring(index + 1);
    	}
    }

    
    
    
    
}
